Obsidian Internal Link를 Next.js에서 작동시키는 방법에 대한 고찰

2025-02-01

옵시디언에서 사용하는 마크다운 문법을 변환하려고 보면 제대로 작동하지 않는 몇가지 문법들이 있는데 대표적인것이 바로 Wiki Link ([Wiki Link](Wiki%20Link)) 그리고 highlight ('==highlight==') 이다. highlight의 경우, remark-mark-highlight를 사용하면 간단하게 적용을 할 수 있지만, 위키링크의 경우 플러그인 설정! 짠! 하고 나오는게 아니었고 ... 추가적으로 설정이 필요했는데 그 삽질 과정을 기록해볼까 한다.

1. 옵시디언 플러그인 사용

obsidian-link-converter 라는 옵시디언 플러그인을 사용하여 명시적으로 경로를 설정해주는 방법. 해당 플러그인을 사용하면 세가지 옵션(Relative Path, Absolute Path, Shortest Path) 중 하나로 Wiki Link를 Markdown Link([]())형식으로 변환해준다.

[위키링크](위키링크)
Relative Path : [위키링크](위키링크.md)
Absolute Path : [위키링크](.../.../위키링크.md)

나는 Absolute Path로 변환을 해서 문서를 작업해서 올렸고 ...여기서 큰 단점이 발생하고 마는데 ...

저렇게 띨롱 변환해서 넘겨버리면 경로 자체가 href로 넘어가게 되어버린다. 당연히 웹에서 링크를 클릭을 해도 경로를 찾을 수가 없으니 에러를 뱉는다. 그래서 고안해낸 방법이 Middleware을 거쳐서 페이지를 이동시키는 것이었다.

middleware.ts
import { NextRequest, NextResponse } from "next/server";
 
 
// href가 Suffix/카테고리/파일명.md라고 가정
 
function removeLeadingAndExtension(str: string) {
	return str.replace('Suffix/', "").replace(/\.md$/, "");
}
 
export function middleware(req: NextRequest) {
	const url = req.nextUrl.clone();
	const pathName = req.nextUrl.pathname;
	const pathRegex = "Suffix[^?]*.md$";
 
	if (pathName.endsWith(".md")) {
		const [pathValue] = pathName.match(pathRegex) ?? "";
		const decodedPath = decodeURIComponent(pathValue);
		url.pathname = "/post/" + removeLeadingAndExtension(decodedPath);
		
		return NextResponse.redirect(url);
	}
	
	return NextResponse.next();
}

하지만 이 방법에도 단점은 있었는데 ... 소스코드에 파일 경로가 전부 다 노출이 된다.

300 최종절망

아무리 내 옵시디언 레포가 Private이라지만 전체 경로가 다 보이니 여간 찝찝한게 아니어서 바꿀 결심을 했다.

2. remark-wiki-link 사용

역시 위키링크 변환에 대한 수요가 있었는지 위키링크 형식을 노드로 파싱해서 <a>로 렌더링을 해주는 플러그인이 이미 존재했다.

해당 플러그인을 사용을 하면 아래와 같은 마크다운 AST의 노드로 변환이 되고

{
    value: 'Test Page',
    data: {
        alias: 'Test Page',
        permalink: 'test_page',
        exists: false,
        hName: 'a',
        hProperties: {
            className: 'internal new',
            href: '#/page/test_page'
        },
        hChildren: [{
            type: 'text',
            value: 'Test Page'
        }]
    }
}

이렇게 html로 렌더링이 된다

<a class="internal new" href="#/page/test_page">Test Page</a>

현재 사용하고 있는 블로그의 경우, 글의 상세페이지 경로는 /post/[글제목] 형식으로 되어있어서 config option도 같이 수정을 해 주었다. 제공하는 옵션들은 아래와 같다.

  • permalinks [String]
    • 존재하는 페이지로 간주할 permalinks를 설정
    • 위키링크 파싱 시, permalink가 이 배열 중 하나와 일치하면 해당 노드의 data.exists속성이 true로 설정됨
  • pageResolver (pageName: String) => [String]
    • 위키링크를 실제 경로나 url로 매핑
//Default
(name) =>[name.replace(/ /g, '_').toLowerCase()]
  • hrefTemplate (permalink: String) => String
    • 파싱할 때 얻은 permalink를 a태그의 href 속성으로 설정할 때 사용
//Default
(permalink) => `#/page/${permalink}`
  • wikiLinkClassName
    • 위키링크의 className을 설정. 기본값은 internal
  • newClassName
    • 링크가 options.permalinks에 없는 경우에 추가되는 className. 기본값은 new
  • aliasDivider
    • 위키링크 안에서 alias를 구분할 때 사용하는 구분자.
<MDXRemote
	source={content}
	options={{
		parseFrontmatter: true,
		mdxOptions: {
			remarkPlugins: [
				//...remark plugins
				[
					remarkWikiLink,
					{
						hrefTemplate: (permalink: string) => permalink,
						pageResolver: (text: string) => [text],
					},
				],
			],
			rehypePlugins: [
				//...rehype plugins
			],
		},
	}}
/>

이런 위키링크를 ([[Obsidian Internal Link를 Next.js에서 작동시키는 방법에 대한 고찰]]) HTML에 렌더링해서 확인해보면?

<a class="internal-link" href="Obsidian Internal Link를 Next.js에서 작동시키는 방법에 대한 고찰">Obsidian Internal Link를 Next.js에서 작동시키는 방법에 대한 고찰</a>

예상대로 잘 작동한다!

번외 : rehype-rewrite

옵시디언 플러그인에서 remark wiki link로 넘어가기 전에 찍먹했던 플러그인이다. 이 친구로 말할 것 같으면 ... element를 수정할 수 있게 하는 플러그인인데, 특정 태그에 해당하는 노드를 불러와 속성을 바꾸거나, 요소를 감싸거나 하는 등의 작업을 할 수 있다.

그 말인 즉슨? 앞에서 고민했던 <a>에 파일 경로가 통으로 넘어가는 문제도 고칠 수 있다는 말이었다.

export declare type RehypeRewriteOptions = {
  /**
   * Select an element to be wrapped. Expects a string selector that can be passed to hast-util-select ([supported selectors](https://github.com/syntax-tree/hast-util-select/blob/master/readme.md#support)).
   * If `selector` is not set then wrap will check for a body all elements.
   */
  selector?: string;
  /** Rewrite Element. */
  rewrite(node: Root | RootContent, index: number | null, parent: Root | Element | null): void;
};

이 옵션을 보면 알 수 있듯이, selector을 통해서 태그를 불러온 다음에 rewrite함수로 수정을 하는 식이었는데, 나는 이렇게 적용을 했었다.

<MDXRemote
	source={content}
	options={{
		parseFrontmatter: true,
		mdxOptions: {
			remarkPlugins: [
				//...remark plugins
			],
			rehypePlugins: [
				//...rehype plugins
				[
					rehypeRewrite,
					{
						selector: "a",
						rewrite: (node: Node) => rewriteLinkNodes(node),
					},
				],
			],
		},
	}}
/>
 
 
function rewriteLinkNodes(node: Node) {
	if (node.type === "element" && node.tagName === "a" && node.properties.href) {
		const href = decodeURIComponent(node.properties.href)
		//마크다운 확장자로 위키링크인지 아닌지 구분
		const internalLink = href.endsWith(".md");
		if (internalLink) {
			node.properties.className = "internal-link";
			node.properties.href = path.basename(href, ".md");
		}
	}
	return node;
}

이런 방법에도 불구하고 remark-wiki-link를 사용한 이유는

  1. 옵시디언에서 문서를 작성할 때 위키링크로 작성하는게 기본 설정으로 되어있고
  2. 위키링크로 작성한 파일을 굳이 또 버튼 몇 번 더 눌러서 경로로 변경을 하는게 상당히 귀찮았기 때문
  3. 코드가 지저분해지는 것이 너무너무 싫었어요 ...

잘 만든 바퀴가 있으니 얼마나 다행인가 .... 만약에 근데 저 플러그인을 못찾았다면 바로 주저없이 rehype-rewrite로 노드를 바꾸는 방법을 선택할것이다. .... 하여튼 이번에 이런저런 사소한 설정이 생각보다 안?부드?럽게 넘어가서 조금씩 만져주고 있는데, 이 wiki link만큼 나를 힘들게 한 녀석은 없었다.......그래도 나름 재밌었어요 굿~👍

Ref